package com.corpgovernment.common.linktrace;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.corpgovernment.common.holder.TraceIdHolder;
import com.ctrip.corp.obt.generic.core.context.RequestContext;
import com.ctrip.corp.obt.generic.core.context.TenantContext;
import com.ctrip.corp.obt.generic.core.context.TraceContext;
import com.ctrip.corp.obt.generic.core.context.UserInfoContext;
import com.ctrip.corp.obt.generic.utils.JsonUtils;
import com.ctrip.corp.obt.generic.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author ldma
 * @date 2023/11/20
 */
@Aspect
@Component
@Slf4j
@Order(Integer.MAX_VALUE - 1)
public class HttpLinkTraceAspect {

    @Value("${spring.application.name:}")
    private String serverName;
    @Value("${spring.datasource.clickhouse.url:}")
    private String url;
    @Value("${spring.datasource.clickhouse.username:}")
    private String username;
    @Value("${spring.datasource.clickhouse.password:}")
    private String password;
    @Value("${enable_link_trace:false}")
    private Boolean enableLinkTrace;
    @Autowired
    @Qualifier("linkTraceThreadPool")
    private ThreadPoolExecutor linkTraceThreadPool;

    @Around("@within(com.corpgovernment.common.linktrace.HttpLinkTrace) || @annotation(com.corpgovernment.common.linktrace.HttpLinkTrace)")
    public Object linkTrace(ProceedingJoinPoint joinPoint) throws Throwable {
        if (!Boolean.TRUE.equals(enableLinkTrace)) {
            return joinPoint.proceed();
        }
        // 开启链路追踪
        HttpLinkTraceHolder.setHttpLinkTrace(Boolean.TRUE);
        long startTime = System.currentTimeMillis();
        Object responseParam = null;
        try {
            responseParam = joinPoint.proceed();
        } finally {
            // 获取请求url
            String requestUrl = Optional.ofNullable(RequestContextHolder.getRequestAttributes())
                    .map(ServletRequestAttributes.class::cast)
                    .map(ServletRequestAttributes::getRequest)
                    .map(HttpServletRequest::getRequestURI).orElse(null);
            Object finalResponseParam = responseParam;
            // 异步保存到clickhouse
            CompletableFuture.supplyAsync(() -> {
                process(joinPoint, "controller", startTime, finalResponseParam, requestUrl);
                return null;
            }, linkTraceThreadPool);
        }
        return responseParam;
    }

    @Around(value = "@within(org.springframework.cloud.openfeign.FeignClient)")
    public Object aroundSoaFeign(ProceedingJoinPoint joinPoint) throws Throwable {
        if (!Boolean.TRUE.equals(enableLinkTrace) || !Boolean.TRUE.equals(HttpLinkTraceHolder.getHttpLinkTrace())) {
            return joinPoint.proceed();
        }
        long startTime = System.currentTimeMillis();
        Object responseParam = null;
        try {
            responseParam = joinPoint.proceed();
        } finally {
            // 异步保存到clickhouse
            Object tmp = responseParam;
            CompletableFuture.supplyAsync(() -> {
                process(joinPoint, "soa", startTime, tmp, null);
                return null;
            }, linkTraceThreadPool);
        }
        return responseParam;
    }

    @Around(value = "execution(* com.corpgovernment..*.doPostJSON(..)) " +
            "|| execution(* com.corpgovernment.*.product.service.supplier.SupplierService.queryFlightProduct(..)) " +
            "|| execution(* com.corpgovernment.*.flightintl.service.FlightQueryCommon.queryProducts(..)) " +
            "|| execution(* com.corpgovernment.*.product.service.supplier.SupplierService.queryProductListRTN(..))")
    public Object aroundSupplierCall(ProceedingJoinPoint joinPoint) throws Throwable {
        if (!Boolean.TRUE.equals(enableLinkTrace) || !Boolean.TRUE.equals(HttpLinkTraceHolder.getHttpLinkTrace())) {
            return joinPoint.proceed();
        }
        long startTime = System.currentTimeMillis();
        Object responseParam = null;
        try {
            responseParam = joinPoint.proceed();
        } finally {
            // 异步保存到clickhouse
            Object tmp = responseParam;
            CompletableFuture.supplyAsync(() -> {
                process(joinPoint, "supplier", startTime, tmp, null);
                return null;
            }, linkTraceThreadPool);
        }
        return responseParam;
    }

    private void process(ProceedingJoinPoint joinPoint, String requestType, long startTime, Object responseParam, String requestUrl) {
        try {
            HttpLinkTraceDo httpLinkTraceDo = new HttpLinkTraceDo();
            httpLinkTraceDo.setRequestDuration(System.currentTimeMillis() - startTime);
            httpLinkTraceDo.setUid(getUid());
            httpLinkTraceDo.setTenantId(TenantContext.getTenantId());
            httpLinkTraceDo.setRequestId(TraceContext.getRequestId());
            httpLinkTraceDo.setTraceId(TraceIdHolder.getTraceId());
            httpLinkTraceDo.setRequestType(requestType);
            httpLinkTraceDo.setServiceName(serverName);
            String jsonRequestParam = "";
            try {
                jsonRequestParam = JsonUtils.toJsonString(buildInput(joinPoint));
            } catch (Exception e) {
                jsonRequestParam = Arrays.toString(buildInput(joinPoint));
            }
            httpLinkTraceDo.setRequestParam(Optional.ofNullable(jsonRequestParam).map(item -> item.replace("'", "")).orElse(""));
            httpLinkTraceDo.setResponseParam(Optional.ofNullable(JsonUtils.toJsonString(responseParam)).map(item -> item.replace("'", "")).orElse(""));
            httpLinkTraceDo.setRequestUrl(RequestContext.getCurrentContext().getRequestURI());
            if (StringUtils.isNotBlank(requestUrl)) {
                httpLinkTraceDo.setRequestUrl(requestUrl);
            }
            // soa特殊处理
            Boolean saveFlag = Boolean.TRUE;
            if (Objects.equals(requestType, "soa")) {
                MethodSignature signature = (MethodSignature) joinPoint.getSignature();
                FeignClient feignClient = (FeignClient) signature.getDeclaringType().getAnnotation(FeignClient.class);
                if (feignClient != null && !Objects.equals(feignClient.name(), serverName)) {
                    httpLinkTraceDo.setServiceName(feignClient.name());
                    // url
                    Method method = signature.getMethod();
                    PostMapping postMapping = method.getAnnotation(PostMapping.class);
                    GetMapping getMapping = method.getAnnotation(GetMapping.class);
                    RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                    if (postMapping != null) {
                        httpLinkTraceDo.setRequestUrl(postMapping.value()[0]);
                    } else if (getMapping != null) {
                        httpLinkTraceDo.setRequestUrl(getMapping.value()[0]);
                    } else if (requestMapping != null) {
                        httpLinkTraceDo.setRequestUrl(requestMapping.value()[0]);
                    }
                } else {
                    saveFlag = Boolean.FALSE;
                }
            }
            if (Boolean.TRUE.equals(saveFlag)) {
                log.info("save http link trace: {}", httpLinkTraceDo);
                saveHttpLinkTrace(httpLinkTraceDo);
            }
        } catch (SQLException e) {
            log.error("save http link trace error", e);
        }
    }

    private Object[] buildInput(ProceedingJoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        Object[] printArgs = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            if ((args[i] instanceof HttpServletRequest) || args[i] instanceof HttpServletResponse) {
                continue;
            }
            if (args[i] instanceof byte[]) {
                printArgs[i] = "byte array";
            } else if (args[i] instanceof MultipartFile) {
                printArgs[i] = "file";
            } else {
                printArgs[i] = args[i];
            }
        }
        return printArgs;
    }

    public void updateHttpLinkTrace(String requestUrl, JSONObject responseParam){
        log.info("is_link_trace: {}", HttpLinkTraceHolder.getHttpLinkTrace());
        if (Boolean.TRUE.equals(HttpLinkTraceHolder.getHttpLinkTrace())) {
            CompletableFuture.supplyAsync(() -> {
                log.info("update http link trace: {}", TraceContext.getRequestId());
                String jsonResponseParam = Optional.ofNullable(responseParam.toString()).map(item -> item.replace("'", "")).orElse("");
                try (Connection connection = DriverManager.getConnection(url, username, password)) {
                    Statement statement = connection.createStatement();
                    String sql = StrUtil.format("update http_link_trace set response_param = '{}' where request_url = '{}' and request_type = 'controller' and request_id = '{}'", jsonResponseParam, requestUrl, TraceContext.getRequestId());
                    statement.execute(sql);
                } catch (Exception e) {
                    log.error("save http link trace error", e);
                }
                return null;
            }, linkTraceThreadPool);
        }
    }

    public void saveHttpLinkTrace(HttpLinkTraceDo httpLinkTraceDo) throws SQLException {
        try (Connection connection = DriverManager.getConnection(url, username, password)) {
            try (Statement statement = connection.createStatement()) {
                String sql = StrUtil.format("insert into http_link_trace(uid, tenant_id, request_id, trace_id, request_type, service_name, request_url, request_param, response_param, request_duration) values('{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}', {})", httpLinkTraceDo.getUid(), httpLinkTraceDo.getTenantId(), httpLinkTraceDo.getRequestId(), httpLinkTraceDo.getTraceId(), httpLinkTraceDo.getRequestType(), httpLinkTraceDo.getServiceName(), httpLinkTraceDo.getRequestUrl(), httpLinkTraceDo.getRequestParam(), httpLinkTraceDo.getResponseParam(), httpLinkTraceDo.getRequestDuration());
                statement.execute(sql);
            }
        }
    }

    private String getUid() {
        String uid = "unknown";
        if (UserInfoContext.getContextParams("uid") != null) {
            return UserInfoContext.getContextParams("uid").toString();
        }
        if (UserInfoContext.getContextParams("userId") != null) {
            return UserInfoContext.getContextParams("userId").toString();
        }
        return uid;
    }

}
